"""This module within the PyMak package defines the Target class.
"""
# Python imports
import traceback
import types
import logging

# Package imports
from path import to_path
from variant import VariantValues
import pmutils
from action import Action

__all__ = [
    'Target',
    ]

DEPENDENCY_GENERATION_ACTION = ".mkdepend"



class DependencyList(list):
    """ This class derives from 'list'. The list contains tuples of the form
    (target, variant-conditions). A Set is also maintained to make the lookup
    of the target faster.
    """
    def __init__(self):
        self.target_set = set()

    def add_dependency(self, target, variantConditions=None):
        if target not in self.target_set:
            self.append( (target, variantConditions) )

    def list_of_sub_targets(self, variants=None):
        return [ d[0] for d in self if d[1] is None or d[1].match_variants(variants) ]

    def string_of_target_files(self, variants=None):
        return " " + " ".join([t.path for t in self.list_of_sub_targets(variants)])

    def targets_with_id(self, id, variants=None, fail=True):
        result = [ t for t in self.list_of_sub_targets(variants) if t.target_id == id ]
        if not result and fail:
            raise KeyError, "Failed to find targets with id = " + id
        return result

    def one_target(self, variants=None, fail=True):
        result = self.list_of_sub_targets(variants)
        if not result:
            raise pmutils.PyMakException("No dependent targets found")
        if fail and len(result) != 1:
            raise pmutils.PyMakException("More than one dependent targets found")
        return result[0]

#    def __getitem__(self, index):
#        return self[index][0]


UNKNOWN_LOCATION = ('<unknown>', 0)

class Target(object):
    """The Target object is the heart of the PyMak4 system.

    The most important attribute of a Target instance is its 'path',
    which is an instance of the pymak.path object. This is a
    string-derived object which implments many of the file and
    directory based methods found in os and os.path on it. Since it is
    a string, it can be passed into anything else which is expecting a
    string. See the documentation for the pymak.path class.

    The most important method on a Target is the 'set' method, which
    is used to modify, through kwargs arguments, most aspects of the
    target.

    A target describes a 'thing' that needs to be built from other
    targets. Some targets refer to pre-existant files (source files),
    in which case there's nothing that needs to be done to build
    them. Other targets describe files that need to be made, in which
    case the target needs some action to define how this gets made.

    Since Actions are often dependent on a particular tree of targets
    in order for them to work correctly, the creation of the targets
    is often left to Rules. The factory method 'target' on the context
    object uses implicit or explicit rules to make the target(s).
    """
    def __init__(self, _context, _path, **kwargs):
        """Constructor for the Target. A convenience factory method,
        called 'target', exists on the context object that calls this
        constructor. Most .pmk files in a component will use the
        factory method.

        See the definition of the set() method for the arguments that
        can be included in kwargs.

        If the kwargs includes a 'context' then this context is used
        rather than the one supplied as the first parameter. This is
        useful to enable make files to override a rule's context -
        most useful when a target has to be be exposed to a parent
        context. Note that the set() method cannot have the context
        supplied - it can only be changed at target creation time.

        Attributes that start with _ tend to be those useful to
        Actions rather than the component's PyMak files.
        """

        # If the key 'context' is in the list of kwargs, it overrides
        # the first parameter
        _context = kwargs.pop('context', _context)

        self.path = to_path(_path)
        self._context = _context
        self.target_id = ""

        # Configuration
        self._built = False
        self._timeStamp = -1 # this means the time of the target is not known
        self._forceRebuild = False
        self._implements_variants = VariantValues()
        self._depends_on = DependencyList()
        self._action = None # an Action-derived instance, or callable
        self._rule = None # refers to the rule that created this target, if any
        self._recurse = True # only sneaky targets should mess with this
        self._is_dir = None # gets initialized in the first set() call

        # Runtime state
        self.__expanded = False
        self.__src = UNKNOWN_LOCATION
        for f, l, _, _ in traceback.extract_stack():
            if f.endswith('.pmk'):
                self.__src = (f, l)

        # Unpack all the optional args
        self.set(**kwargs)

        # register with the context _after_ the kwargs have been handled since
        # they can change the path.
        _context.all_targets[self.path] = self


    def set(self,
            target_dir=None,
            target_file=None,
            target_extn=None,
            variants=None,
            action=None,
            target_id=None,
            force_rebuild=None,
            depends_on=None,
            variant_conditions=None,
            target_args=None,
            is_dir=None,
            attrs=None,
            ):
        """This method is used to set up the attributes of the target
        in the most flexible way possible. The constructor calls this
        method with the most of its arguments forwarded on to it.

        - target_dir, target_file, target_extn: if these are
        specified, then the target's path attribute is modified
        accordingly.

        - variants: this parameter sets the list of variants that this
        target varies by.

        - action: this parameter is either an Action object, a
        callable, or a string that is looked up in the target's
        context. The callable will be called with the parameters
        'target' and 'variant_values', exactly as the execute_action
        method on an Action instance would.

        - target_id: this parameter is not used by the core PyMak
        system - it is an attribute of a target that actions and
        client code are free to use for their own purpose.

        - force_rebuild: this parameter forces the target to be
        rebuilt regardless of the timestamp on the target file.

        - depends_on: This parameter describes what this target is
        dependent on. The value can either be a single thing, a list,
        a tuple or a generator of things. The things can either be a
        string representing a path from which a target can be created,
        or a instance of a Target object.

        - variant_conditions: this parameter is used if depends_on is
        supplied, and ignored otherwise. It defines the variant
        conditions that will be established between this target and
        any targets referenced in the depends_on parameter.

        - target_args: this parameter is only used if depends_on is
        supplied, and a path, rather than a Target object, is
        referenced. When the 'target' factory method is called on the
        context, this dictionary value is passed as the **kwargs to
        that method.

        - is_dir: sometimes a target is used to describe a
        directory. If this parameter is set, then the _is_dir
        attribute of the target instance is initialized to that value,
        otherwise the attribute value is determined by looking at the
        target path. If the path exists and is a directory, then the
        attribute is set to True. Otherwise it is set to False. The
        value of this attribute determines how the automatic folder
        construction works when the target is being built.

        - attrs: a dictionary of extra attributes that get set on
        the target. These are usually for setting attributes in rules
        so that actions can use them.
        """

        if variants is not None:
            self._implements_variants.reconcile_all(self._context.variants.sub(variants))

        if target_id is not None:
            self.target_id = target_id # this field can be used by the rules/actions to tag the target with arbitrary data

        if action is not None:
            if isinstance(action, basestring):
                action = self._context.all_actions.get(action)
                if not action:
                    raise pmutils.PyMakException("Unkown action value: %s" % action)
            elif isinstance(action, Action):
                pass
            elif callable(action):
                pass
            else:
                raise pmutils.PyMakException("Unkown type for action attribute: %s" % str(action))
            self._action = action

        if target_dir is not None:
            self.path = (to_path(target_dir) / self.path.name).normpath()

        if target_file is not None:
            self.path = (self.path.parent / to_path(target_file)).normpath()

        if target_extn is not None:
            self.path = to_path(self.path.namebase + target_extn)

        if depends_on is not None:
            if not isinstance(depends_on, list) and not isinstance(depends_on, tuple) and type(depends_on) != types.GeneratorType:
                depends_on = [ depends_on ]
            for d in depends_on:
                if isinstance(d, basestring):
                    if target_args is None:
                        target_args = {}
                    d = self._context.target(d, **target_args)
                elif isinstance(d, Target):
                    pass
                else:
                    raise pmutils.PyMakException("Unknown argument to depends on: %s" % str(d))
                self._depends_on.add_dependency(d, variant_conditions)

        if force_rebuild is not None:
            self._forceRebuild = True

        if is_dir is not None:
            assert isinstance(is_dir, bool)
            self._is_dir = is_dir
        elif self._is_dir is None:
            # first time being initialized
            self._is_dir = self.path.isdir()

        if attrs:
            for k, v in attrs.items():
                setattr(self, k, v)
        return self


    def pymak_src(self):
        """This method returns a string describing the source pymak
        file (.pmk extension) and the line number that the target was
        created from. This is determined at target creation time by
        looking in the execution stack for the first entry which comes
        from a pymak file. This means that even if the target gets
        constructed deep down in series of utilities and rules, the
        point in the pymak file that started this chain of execution
        is stored.
        """
        return "File: '%s', line: %d" % self.__src

    def __str__(self):
        if self._context.verbosity >= 2:
            return "Target('%s',variants=%s,depends_on=%s,action=%s,target_id=%s)" % (self.path, str(self._implements_variants), str(self._depends_on), str(self._action), repr(self.target_id))
        elif self._context.verbosity >= 1:
            return "Target('%s',variants=%s,depends_on=%s,action=%s)" % (self.path, str(self._implements_variants), str(self._depends_on), str(self._action))
        else:
            return "Target('%s')" % self.path

    def __repr__(self):
        return str(self)


    def _build(self):
        """ This is the heart of the 'make' process which walks over
        the tree of targets and builds the targets that need to be
        built. This method should not be called by pymak files - the
        wrapper context will call this on each of the targets that are
        refered to on the command line.
        """
        # build a new VariantValues which contains the
        # values of the variants that this target implements
        # build what this target depends on
        sub_targets = self._depends_on.list_of_sub_targets()
        if self._recurse:
            for targ in sub_targets:
                targ._build()
        else:
            for targ in sub_targets:
                targ.expand_target()

        # expand paths on this target
        self.expand_target()

        # now build the target
        if not self._check_target_uptodate(sub_targets):
            if self._action:
                success = False
                try:
                    self._call_action()
                    success = True
                    self._built = True
                    self._time_stamp()
                finally:
                    # delete the target since leaving parially created targets
                    # can cause big problems in subsequent makes
                    if not success:
                        if self.path.isfile():
                            try:
                                self.path.unlink()
                            except:
                                pass
            else:
                self._built = True
                self._time_stamp()


    def _call_action(self):
        """This utility method simply calls the target's action
        object's execute_action method. If that method raises the
        CommandFailedException and pymak4 is running in the keep-going
        mode, then the problem is reported, and the exception is
        squashed. Otherwise the exception is passed on up to the
        caller.
        """
        try:
            if isinstance(self._action, Action):
                self._action.execute_action(self, self._implements_variants)
            else:
                self._action(self, self._implements_variants)
        except pmutils.CommandFailedException:
            if self._context.keep_going:
                logging.error(_("Ignoring failed command for target '%s'"), self.path)
                logging.error(str(e))
            else:
                raise
            

    def _check_target_uptodate(self, sub_targets):
        """This method is used by the internal PyMak mechanism to
        determine if the target is up-to-date. The algorithm used is:

        - if the target has been built previously in the run of PyMak,
        it returns True.

        - if any of its dependent targets were built previously in
        this run of PyMak, then it returns False - i.e. since a
        dependent target has been built, this target needs to be
        rebuilt.

        - if the force_rebuild flag has been set, then the method
        returns False.

        - if the target's path does not exist, then the method returns
        False.

        - if the target's path does exist, but its time stamp is
        earlier than any of its dependent targets' time stamps, then
        this method returns False.

        - otherwise it returns True - the target must be up-to-date.
        """
        # check to see if a dependent target was rebuilt
        if self._built:
            return True  # this target built already
        for sub_targ in sub_targets:
            if sub_targ._built:
                return False # trigger a rebuild of target
        # check time stamps on the target and dependent targets
        if not self.path or self._forceRebuild:
            return False    # always rebuild if no target file
        targ_ts = self._time_stamp()
        if targ_ts == 0:
                return False  # the file does not exist, so build it
        for sub_targ in sub_targets:
                sub_targ_ts = sub_targ._time_stamp()
                if sub_targ_ts > targ_ts:
                        return False   # dependent is newer than this target
        return True # it must be up-to-date

    def _time_stamp(self):
        """ Return the time stamp of the target's file. If the time
        stamp is not known yet, then it looks on the file system for
        its modified time, and caches this value so that it doesn't
        need to be fetched again."""
        if self._timeStamp > 0:
            return self._timeStamp
        try:
            ts = self.path.mtime
        except:
            ts = 0
        self._timeStamp = ts
        return ts

    def expand_target(self):
        """This method is called by the pymak mechanism to expand
        variables and properties in the target's path string. Most of
        the time this is sufficient and therefore the method does not
        need to be called by pymak files or action methods.
        """
        if not self.__expanded:
            if self._context.path:
                self.path = self._context.path / self.path
            self.path = to_path(pmutils.expand_variables(self.path, self._implements_variants, self._context, self)).normpath()
            fld = self.path
            if not self._is_dir:
                fld = fld.parent
            pmutils.ensure_dir_exists(fld)
            self.__expanded = True

    def init_variant_values(self, variant_values):
        collect = VariantValues()
        for v in self._implements_variants:
            if not variant_values.contains(v):
                raise pmutils.PyMakException("Variant %s required by target %s but not defined." % (v, self.path))
            collect.reconcile(v, variant_values.lookup(v))
        return collect

    def read_all_dependency_files(self, variant_values):
        """The modules that are built in to support things like C/C++
        header dependencies can write out files describing the files
        that that C/C++ files depend on. This method reads those files
        in, constructs targets to represent those header files, and
        sets up the appropriate dependency.

        This method should not be used in a component's pymak files.
        """
        new_variant_values = self.init_variant_values(variant_values)
        for targ in self._depends_on.list_of_sub_targets(new_variant_values):
            targ.read_all_dependency_files(variant_values)

        if self._action == DEPENDENCY_GENERATION_ACTION:
            self.expand_target(new_variant_values)

            actual_target = target.actualTarget
            try:
                logging.debug(_("Reading in file '%s'"), self.path)
                for d in self.path.lines():
                    d = d.strip()
                    if len(d):
                        t = self._context.target(d)
                        actual_target.set(depends_on=t)
            except IOError:
                logging.warning(_("Failed to read in '%s'"), self.path)

    def list_of_sub_targets(self, variants):
        """Convenience method to get the list of sub-targets of this
        target."""
        return self._depends_on.list_of_sub_targets(variants)
